#include <ultra64.h>
#include "lib/tlb.h"
#include "constants.h"
#include "bss.h"
#include "lib/boot.h"
#include "lib/crash.h"
#include "lib/rzip.h"
#include "lib/dma.h"
#include "lib/lib_48150.h"
#include "lib/vm.h"
#include "data.h"
#include "types.h"

/**
 * vm - virtual memory
 *
 * To get around memory limitations, Perfect Dark implements a memory paging
 * system similar to ones found in modern operating systems. Memory is divided
 * into pages and can be accessed by virtual addresses. If the page does not
 * exist in physical memory then the operating system can replace a page in
 * physical memory with the required one and try again. This allows the system
 * to address more memory than is actually loaded and have a smaller memory
 * footprint.
 *
 * All code in the game is divided into two segments: Unpagable and pagable.
 *
 * Unpagable:
 * - Is what decomp calls lib.
 * - Is virtual at 0x70000000 and statically mapped to 0x80000000.
 * - Contains frequently used code which would be a bad idea to swap in and out,
 *   as well as all of libultra.
 *
 * Pagable:
 * - Is what decomp calls game.
 * - Is virtual at 0x7f000000.
 * - Contains the majority of the game code.
 *
 * Paging cannot occur for any sections which are writeable in memory because
 * the N64 ROM is read only. Sections of this type are .data and .bss.
 * These sections reside in physical memory at 0x80000000 and are not pagable.
 *
 * Paging is implemented for the .text (ie. code) and .rodata segments. When
 * building the ROM image, these are grouped into one binary then sliced into
 * chunks of size 4KB (the page size). Each chunk is zipped and then each zip is
 * placed on the ROM along with a zip offset table.
 *
 * The N64 contains a translation lookaside buffer (TLB), a feature of the CPU
 * which can map virtual memory to physical memory. The TLB contains a table of
 * 32 entries. Each entry can map two pages, and PD's page size is 4KB. The
 * unpagable segment described above is mapped using one TLB entry, leaving 31
 * entries for dynamic paging.
 *
 * A page an be in one of three states:
 * - Mapped in the TLB and loaded in physical memory.
 * - Not mapped in the TLB but still loaded in physical memory.
 * - Not mapped in the TLB and not in physical memory
 *   (must be unzipped from the ROM to access).
 *
 * A TLB miss occurs when a request is made for a page that isn't mapped in the
 * TLB. When this happens, the system's exception handler checks if the page
 * exists in physical memory or not. If it does, a TLB entry is created for it.
 *
 * A page miss occurs when the page didn't exist in physical memory. In this
 * case the exception handler must load it from the ROM, unzip the page and
 * replace an existing one.
 *
 * Paging is only implemented for 4MB systems (ie. when the expansion pak is not
 * being used). When using the expansion pak all game code is loaded and the
 * mappings in the TLB are static.
 */

u8 g_Is4Mb;
u32 g_VmNumTlbMisses;
u32 g_VmNumPageMisses;
u32 g_VmNumPageReplaces;
u8 *g_VmMarker;
u32 g_VmRamEnd;
u32 g_VmVirtualToPhysicalTableEnd;

#ifdef DEBUG
u8 g_VmShowStats = false;
#endif

#if VERSION == VERSION_NTSC_BETA
u32 fillnb[2] = {0};
#endif

#if VERSION == VERSION_PAL_BETA
s32 g_VmNumPages = 0;
u32 var8005cf80 = 0;
#else
u32 var8005cf80 = 0;
s32 g_VmNumPages = 0;
#endif

u32 var8005cf88 = 0;

extern u8 EXT_SEG _gameSegmentStart;
extern u8 EXT_SEG _gameSegmentEnd;
extern u8 EXT_SEG _gamezipSegmentRomStart;

extern u32 g_VmPhysicalSlots;
extern u32 *g_VmVirtualToPhysicalTable;
extern u8 g_VmInitialised;
extern u32 g_VmZipBuffer;
extern u32 *g_VmZipTable;

/**
 * Initialise the virtual memory.
 *
 * The logic here is different depending on whether the system has the 4MB
 * of onboard memory or is using the expansion pak for a total of 8MB.
 *
 * -- For 4MB systems --
 *
 * vmInit allocates space in memory for the TLB to be able to load zips in its
 * exception handler. It initialises the zip table then leaves it to the TLB to
 * load the game zips as needed.
 *
 * The memory is laid out like this:
 *
 *              (zip buffer) (zip table) (state table) (stack)    (end of onboard memory)
 *   Addresses:              0x???       0x???         0x803f50b8 0x80400000
 *     Lengths:              0x6ec       0xdd8         0xaf48     0
 *
 * zip buffer - is sized according to the biggest single game zip, and is
 *     reserved space where the TLB's exception handler can DMA the zip to RAM
 *     before unzipping it.
 * zip table - is the ROM offset table where each zip can be found, which is
 *     used by the TLB's exception handler.
 * state table - is cleared by vmInit then left to the TLB's exception handler
 *     for it to populate as zips are loaded and paged out.
 * stack - is reserved stack space for different threads, which vmInit must not
 *     write into.
 *
 * -- For 8MB systems --
 *
 * vmInit loads all game zips into memory and sets TLB entries to map it to
 * virtual address space. The page swapping feature is not used as the TLB
 * never encounters a page miss.
 *
 * The memory is laid out like this:
 *
 *              (zip buffer) (zip table) (game seg) (stack)    (end of onboard memory)
 *   Addresses:              0x???       0x80220000 0x803f50b8 0x80400000
 *     Lengths:              0x6ec       0x1b99e0   0xaf48     0
 *
 * zip buffer: is sized as VM_PAGE_SIZE * 2, which guarantees it's big enough to
 *     hold any zip.
 * zip table: is the ROM offset table where each zip can be found.
 * game seg: is where the entire game segment is unzipped to.
 * stack: is reserved stack space for different threads, which vmInit must not
 *     write into.
 *
 * -- Both systems --
 *
 * Regardless of the amount of memory being used, it is critical that vmInit
 * sets the g_VmMarker global variable correctly. This marks the point in memory
 * where memory must be preserved. The main thread uses this variable as the end
 * address of memp's heap.
 *
 * In 4MB, g_VmMarker is set to the start of the zip buffer because the zip
 * buffer is used by the exception handler.
 *
 * In 8MB, the zip buffer and zip table are no longer needed, so g_VmMarker is
 * set to the start of the unzipped game segment.
 */
void vmInit(void)
{
	s32 s1;
	u32 *romaddrs;
	u32 numpages;
	u8 *s2;
	u8 *s1p;
	u8 *chunkbuffer;
	u8 *s7; // nb: 154c
	u8 *gameseg;
	u8 *zip; // 48
	s32 pagenum;
	s32 statetablelen;
#if VERSION < VERSION_NTSC_1_0
	u32 stack1;
#endif
	u32 size;
	u32 numentries2; // N/A, 1474
	u32 numentries;
	u32 *ptr;
#if VERSION >= VERSION_NTSC_1_0
	s32 i;
	u8 sp68[1024 * 5]; // 128, 68
#else
	u8 sp68[1024 * 5]; // 128, 68
	char message[128]; // nb: a8
	s32 len;
	s32 stack2;
#endif

	g_VmInitialised = true;

	rzipInit();

#if VERSION >= VERSION_NTSC_1_0
	if (bootGetMemSize() <= 0x400000)
#else
	if (osGetMemSize() <= 0x400000)
#endif
	{
		g_Is4Mb = true;

		g_VmNumPages = (s32)((REF_SEG _gameSegmentEnd - REF_SEG _gameSegmentStart) + (VM_PAGE_SIZE - 1)) / VM_PAGE_SIZE;

		g_VmRamEnd = 0x7f000000 + VM_PAGE_SIZE * g_VmNumPages;
		g_VmVirtualToPhysicalTableEnd = STACK_START;
		gameseg = (u8 *) (STACK_START - g_VmNumPages * 8);
		g_VmVirtualToPhysicalTable = (u32 *) gameseg;

		numpages = (u32) (((uintptr_t) REF_SEG _gameSegmentEnd - (uintptr_t) REF_SEG _gameSegmentStart) + (VM_PAGE_SIZE - 1)) / VM_PAGE_SIZE;
		numentries = numpages + 1;

		g_VmZipTable = (u32 *) ((uintptr_t) ((u32 *) gameseg - (numentries + 4)) & ~0xf);

		// Load gamezips pointer list
		dmaExec(g_VmZipTable, (romptr_t) REF_SEG _gamezipSegmentRomStart, ALIGN16((numentries + 1) << 2));

		// Make pointers absolute instead of relative to their segment
		for (pagenum = 0; pagenum < numentries; pagenum++) {
			g_VmZipTable[pagenum] += (romptr_t) REF_SEG _gamezipSegmentRomStart;
		}

		// Find the size of the biggest compressed zip
		s1 = 0;

		for (pagenum = 0; pagenum < numentries - 1; pagenum++) {
			size = g_VmZipTable[pagenum + 1] - g_VmZipTable[pagenum];

			if (size > s1) {
				s1 = size;
			}
		}

		s1 += 0x40;
		s1 &= (u32) ~0xf;
		g_VmZipBuffer = (uintptr_t) g_VmZipTable - s1;
		g_VmZipBuffer &= ~0xf;
		gameseg = (u8 *) (g_VmZipBuffer - VM_NUM_SLOTS * VM_PAGE_SIZE);
		gameseg -= (uintptr_t) gameseg & 0x1fff;
		g_VmPhysicalSlots = (uintptr_t) gameseg;
		g_VmMarker = gameseg;

		vmInitVars();

		// Clear the state table
		ptr = g_VmVirtualToPhysicalTable;
		statetablelen = (g_VmNumPages * 8) >> 2;

		for (s1 = 0; s1 < statetablelen; s1++) {
			ptr[s1] = 0;
		}

		vmInitVacant();
	} else {
		// Expansion pak is being used
		g_Is4Mb = numentries * false;

		numpages = (u32)((REF_SEG _gameSegmentEnd - REF_SEG _gameSegmentStart) + (VM_PAGE_SIZE - 1)) / VM_PAGE_SIZE;
		s7 = (u8 *) STACK_START;

#if VERSION >= VERSION_NTSC_1_0
		numentries = numpages + 1;
		gameseg = (u8 *) ((uintptr_t) (s7 - (u8 *) ALIGN64((uintptr_t) REF_SEG _gameSegmentEnd - (uintptr_t) REF_SEG _gameSegmentStart)) & 0xfffe0000);
#else
		gameseg = (u8 *) ((uintptr_t) (s7 - (u8 *) ALIGN64((uintptr_t) REF_SEG _gameSegmentEnd - (uintptr_t) REF_SEG _gameSegmentStart)) & 0xfffe0000);
		numentries = numpages + 1;
#endif

		romaddrs = (u32 *) (((uintptr_t) gameseg - ((numentries + 4) << 2)) & ~0xf);
		g_VmMarker = gameseg;
		numentries2 = numentries;

		// Load gamezips pointer list
		dmaExec(romaddrs, (romptr_t) REF_SEG _gamezipSegmentRomStart, ALIGN16((numentries2 + 1) << 2));

		if (pagenum);

#if VERSION >= VERSION_NTSC_1_0
#define ITER i
#else
#define ITER s1
#endif

		// Make pointers absolute instead of relative to their segment
		for (ITER = 0; ITER < numentries2; ITER++) { \
			romaddrs[ITER] += (romptr_t) REF_SEG _gamezipSegmentRomStart;
		}

		// Load each zip from the ROM and inflate them to the game segment
		s2 = gameseg;
		chunkbuffer = (u8 *) ((uintptr_t) romaddrs - VM_PAGE_SIZE * 2);
		zip = chunkbuffer + 2;

		for (ITER = 0; ITER < numentries2 - 1;) {
			dmaExec(chunkbuffer, romaddrs[ITER], ALIGN16(romaddrs[ITER + 1] - romaddrs[ITER]));

#if VERSION >= VERSION_NTSC_1_0
			s2 += rzipInflate(zip, s2, sp68);
#else
			len = rzipInflate(zip, s2, sp68);

			if (len == 0) {
				sprintf(message, "DMA-Crash %s %d Ram: %02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
						"vm_m.c", 298,
						chunkbuffer[0], chunkbuffer[1], chunkbuffer[2], chunkbuffer[3],
						chunkbuffer[4], chunkbuffer[5], chunkbuffer[6], chunkbuffer[7],
						chunkbuffer[8], chunkbuffer[9], chunkbuffer[10], chunkbuffer[11],
						chunkbuffer[12], chunkbuffer[13], chunkbuffer[14], chunkbuffer[15]);
				crashSetMessage(message);
				CRASH();
			}

			s2 += len;
#endif

			ITER++;
		}

		// This loop sets the following TLB entries:
		// entry 2: 0x7f000000 to 0x7f010000 and 0x7f010000 to 0x7f020000
		// entry 3: 0x7f020000 to 0x7f030000 and 0x7f030000 to 0x7f040000
		// ...
		// entry 14: 0x7f1a0000 to 0x7f1b0000 and 0x7f1b0000 to 0x7f1c0000
		s1p = (u8 *) 0x7f000000;
		pagenum = 2; // reusing variable

		while (gameseg <= s7) {
			osMapTLB(pagenum, OS_PM_64K, s1p,
					osVirtualToPhysical((void *) gameseg),
					osVirtualToPhysical((void *) (gameseg + 0x10000)), -1);

			gameseg += 0x20000;
			s1p += 0x20000;
			pagenum++;
		}
	}

	g_VmNumTlbMisses = 0;
	g_VmNumPageMisses = 0;
	g_VmNumPageReplaces = 0;

	osInvalICache(0, ICACHE_SIZE);
}
